package exec

import (
	"context"
	"fmt"
	"io"
	"log"
	"os"
	"os/exec"
	"runtime"
	"strings"
	"sync"

	"github.com/pkg/errors"
)

type Command struct {
	name   string
	args   []string
	osc    func(c *Command)
	bg     bool
	dir    string
	stdIn  io.Reader
	stdOut io.Writer
	errOut io.Writer

	cur    *exec.Cmd
	err    error
	wait   sync.WaitGroup
	locker sync.Mutex
}

func New(name string, args ...string) *Command {
	if runtime.GOOS == "windows" && !strings.HasSuffix(name, ".exe") {
		name += ".exe"
	}
	return &Command{name: name, args: args, stdOut: os.Stdout, errOut: os.Stderr, stdIn: os.Stdin}
}

func Go(args ...string) *Command {
	_ = os.Setenv("GO111MODULE", "on")
	_ = os.Setenv("GOPROXY", "https://goproxy.cn,direct")
	return New("go", args...)
}

func (c *Command) String() string {
	return fmt.Sprintf("%s %s", c.Name(), strings.Join(c.Args(), " "))
}

func (c *Command) Name() string {
	if c.name != "" {
		return c.name
	}
	return "<nil>"
}

func (c *Command) Arg(i int) string {
	if len(c.args) > i {
		return c.args[i]
	}
	return "<nil>"
}

func (c *Command) Args() []string {
	return c.args
}

func (c *Command) Start(ctx context.Context) *Command {
	c.locker.Lock()
	c.cur = exec.CommandContext(ctx, c.name, c.args...)
	if c.dir != "" {
		c.cur.Dir = c.dir
	} else {
		c.cur.Dir, _ = os.Getwd()
	}
	c.cur.Env = os.Environ()
	c.cur.Stdin = c.stdIn
	c.cur.Stdout = c.stdOut
	c.cur.Stderr = c.errOut
	c.cur.SysProcAttr = NewProcessGroup()
	c.locker.Unlock()
	c.handleSC()

	c.err = c.cur.Start()
	if c.err != nil {
		c.handleSC()
		return c
	}

	log.Printf("[%d] %s\n", c.cur.Process.Pid, c.cur.String())

	if !c.bg {
		c.err = c.cur.Wait()
		c.handleSC()
		return c
	}

	c.wait.Add(1)
	go func() {
		defer c.wait.Done()
		c.err = c.cur.Wait()
		c.handleSC()
	}()
	return c
}

func (c *Command) Run(ctx context.Context) error {
	return c.Start(ctx).Wait()
}

func (c *Command) Kill() error {
	err := func() error {
		c.locker.Lock()
		defer c.locker.Unlock()
		if c.err != nil {
			return errors.WithStack(c.err)
		}
		if c.cur != nil && c.cur.Process != nil {
			log.Println("kill process: ", c.Name(), c.cur.Process.Pid)
			pid := c.cur.Process.Pid
			defer func(pid int) {
				if err := errors.WithStack(KillAll(pid)); err != nil {
					log.Printf("%+v", err)
				}
			}(pid)
			return errors.WithStack(c.cur.Process.Kill())
		}
		return nil
	}()
	if err != nil {
		return err
	}
	return c.Wait()
}

func (c *Command) Wait() (err error) {
	c.locker.Lock()
	if c.err != nil {
		c.locker.Unlock()
		return errors.WithStack(c.err)
	}
	c.locker.Unlock()
	c.wait.Wait()
	return
}

func (c *Command) Status() (s Status) {
	c.locker.Lock()
	defer c.locker.Unlock()
	s.Err = errors.WithStack(c.err)
	switch {
	case c.cur.ProcessState != nil:
		s.S = Exited
		s.Pid = c.cur.ProcessState.Pid()
		s.Code = c.cur.ProcessState.ExitCode()
		s.Exited = c.cur.ProcessState.Exited()
	case c.cur.Process != nil:
		s.S = Running
		s.Pid = c.cur.Process.Pid
	case c.cur != nil:
		s.S = Ready
	default:
		s.S = None
	}
	return
}

func (c *Command) handleSC() {
	if c.osc != nil {
		c.osc(c)
	}
}
